package cn.wolfcode.service.impl;

import cn.wolfcode.common.exception.BusinessException;
import cn.wolfcode.common.utils.AsserUtil;
import cn.wolfcode.common.web.CodeMsg;
import cn.wolfcode.common.web.Result;
import cn.wolfcode.domain.Product;
import cn.wolfcode.domain.SeckillProduct;
import cn.wolfcode.domain.SeckillProductVo;
import cn.wolfcode.feign.ProductFeignApi;
import cn.wolfcode.mapper.SeckillProductMapper;
import cn.wolfcode.redis.SeckillRedisKey;
import cn.wolfcode.service.ISeckillProductService;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Slf4j
@Service
@CacheConfig(cacheNames = "SeckillProduct")
public class SeckillProductServiceImpl implements ISeckillProductService {
    @Autowired
    private SeckillProductMapper seckillProductMapper;
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private ProductFeignApi productFeignApi;
    //    @Autowired
//    private RocketMQTemplate rocketMQTemplate;
    @Autowired
    private RedisScript<Boolean> redisScript;
    @Autowired
    private RedissonClient redissonClient;

    @Override
    public List<SeckillProductVo> selectTodayListByTime(Integer time) {
        // 1. 调用秒杀服务接口, 基于今天的时间, 查询今天的所有秒杀商品数据
        List<SeckillProduct> todayList = seckillProductMapper.queryCurrentlySeckillProduct(time);
        if (todayList.size() == 0) {
            return Collections.emptyList();
        }
        // 2. 遍历秒杀商品列表, 得到商品 id 列表
        List<Long> productIdList = todayList.stream() // Stream<SeckillProduct>
                .map(SeckillProduct::getProductId) // SeckillProduct => Long
                .distinct()
                .collect(Collectors.toList());
        // 3. 根据商品 id 列表, 调用商品服务查询接口, 得到商品列表
        Result<List<Product>> result = productFeignApi.selectByIdList(productIdList);
        /**
         *  result 可能存在的几种情况:
         *  1. 远程接口正常返回, code == 200, data == 想要的数据
         *  2. 远程接口出现异常, code != 200
         *  3. 接口被熔断降级, data == null
         */
        if (result.hasError() || result.getData() == null) {
            throw new BusinessException(new CodeMsg(result.getCode(), result.getMsg()));
        }

        List<Product> products = result.getData();

        // 4. 遍历秒杀商品列表, 将商品对象与秒杀商品对象聚合到一起
        // List<SeckillProduct> => List<SeckillProductVo>
        List<SeckillProductVo> productVoList = todayList.stream()
                .map(sp -> {
                    SeckillProductVo vo = new SeckillProductVo();
                    BeanUtils.copyProperties(sp, vo);

                    // 遍历远程查询的商品列表，判断是否与当前的秒杀商品关联的商品对象一致
                    // 如果是一致的，将该对象返回并将属性拷贝到 vo 对象中
                    List<Product> list = products.stream().filter(p -> sp.getProductId().equals(p.getId())).collect(Collectors.toList());
                    if (list.size() > 0) {
                        Product product = list.get(0);
                        BeanUtils.copyProperties(product, vo);
                    }
                    vo.setId(sp.getId());

                    return vo;
                }) // Stream<SeckillProductVo>
                .collect(Collectors.toList());

        return productVoList;
    }

    @Override
    public List<SeckillProductVo> selectTodayListByTimeFromRedis(Integer time) {
        String key = SeckillRedisKey.SECKILL_PRODUCT_LIST.join(time + "");
        List<String> stringList = redisTemplate.opsForList().range(key, 0, -1);

        if (stringList == null || stringList.size() == 0) {
            log.warn("[秒杀商品] 查询秒杀商品列表异常, Redis 中没有数据, 从 DB 中查询...");
            return this.selectTodayListByTime(time);
        }

        return stringList.stream().map(json -> JSON.parseObject(json, SeckillProductVo.class)).collect(Collectors.toList());
    }

    @Override
    @Cacheable(key = "'selectBySeckillId:' + #seckillId")
    public SeckillProductVo selectBySeckillId(String seckillId) {

        SeckillProduct seckillProduct = seckillProductMapper.selectBySeckillId(seckillId);

        Result<List<Product>> result = productFeignApi.selectByIdList(Collections.singletonList(seckillProduct.getProductId()));
        if (result.hasError() || result.getData() == null || result.getData().size() == 0) {
            throw new BusinessException(new CodeMsg(result.getCode(), result.getMsg()));
        }

        Product product = result.getData().get(0);

        SeckillProductVo vo = new SeckillProductVo();

        // 先将商品的属性 copy 到 vo 对象中
        BeanUtils.copyProperties(product, vo);

        // 再将秒杀商品的属性 copy 到 vo 对象中, 并覆盖 id 属性
        BeanUtils.copyProperties(seckillProduct, vo);
        return vo;
    }

    @CacheEvict(key = "'selectBySeckillId:' + #seckillId")
    @Override
    public void decrStockCount(String seckillId) {

        // 方式一：使用jvm内置锁synchronized
//        synchronized (this) {
//            // 查询数据库库存是否足够
//            Integer stockCount = seckillProductMapper.getStockCount(id);
//            AsserUtil.isTrue(stockCount <= 0, "库存不足，请勿下单");
//            // 减库存
//            seckillProductMapper.decrStock(id);
//        }


        //  方式二：使用redis的 SETNX 命令，问题：无法设置过期时间
//        String key = "Seckill:Product:stockCount:" + time + ":" + id;
//        try {
//            Boolean setNx = false;
//            int cycleNum = 5;//自旋次数
//            while (setNx == null || !setNx) {
//
//                // 获取锁，返回null或者false表示获取锁失败
//                // 解决无法设置超时时间：使用LUA脚本发送超时时间
//                setNx = redisTemplate.opsForValue().setIfAbsent(key, "1");
//                // 自旋再次尝试获取锁，自旋次数为5
//                // while (cycleNum-- <= 0) break;
//                AsserUtil.isTrue(cycleNum-- <= 0,"系统繁忙，请稍后再试");
//            }
//
//            // 查询数据库库存是否足够
//            Integer stockCount = seckillProductMapper.getStockCount(id);
//            AsserUtil.isTrue(stockCount <= 0, "库存不足，请勿下单");
//            // 减库存
//            seckillProductMapper.decrStock(id);
//
//        }catch (Exception e){
//            e.printStackTrace();
//        }finally {
//            //释放锁
//            redisTemplate.delete(key);
//        }


        //  方式三：使用redis的SETNX命令和LUA脚本添加过期时间，防止服务宕机死锁；问题：锁过期时间固定，可能事务还未执行完锁被提前释放，造成并发问题
        String key = "Seckill:Product:stockCount:" + seckillId;
        try {
            boolean setNx = false;
            int cycleNum = 5;//自旋次数
            while (!setNx) {

                // 执行LUA脚本，一次性发送[set nx px]命令和超时时间给redis，保证超时释放锁
                setNx = redisTemplate.execute(redisScript, Collections.singletonList(key), "1", "10");

                AsserUtil.isTrue(cycleNum-- <= 0,"已自旋5次尝试获取分布式锁，请稍后再试");

            }

            // 查询数据库库存是否足够
            int stockCount = seckillProductMapper.getStockCount(seckillId);
            AsserUtil.isTrue(stockCount <= 0, "库存不足，请勿下单");
            // 减库存
            seckillProductMapper.decrStock(seckillId);
        }catch (Exception e) {
            log.error(e.getMessage());
        }finally {
            // 释放锁
            redisTemplate.delete(key);
        }

        // 方式四：使用 redisson框架实现分布式锁，它会启动一个后台线程（看门狗线程）：当事务还未执行完，锁会自动续约，保证锁不会提前过期
//        RLock rLock = redissonClient.getLock(key);
//        try{
//            // 获取到锁
//            rLock.lock();
//            // 查询数据库库存是否足够
//            Integer stockCount = seckillProductMapper.getStockCount(seckillId);
//            AsserUtil.isTrue(stockCount <= 0, "库存不足，请勿下单");
//            // 减库存
//            seckillProductMapper.decrStock(seckillId);
//        }catch (Exception e){
//            e.printStackTrace();
//        }finally {
//            // 解锁
//            rLock.unlock();
//        }
    }

    // 乐观锁实现，对库存做版本号控制
    @CacheEvict(key = "'selectByIdAndTime:' + #id")
    @Override
    public void decrStockCountCAS(Long id) {
        // 减库存
        int row = seckillProductMapper.decrStockCAS(id);
        AsserUtil.isTrue(row <= 0,"库存不足，请勿下单，乐观锁控制");
    }
}
